
/*
 * Copyright (C) 2008-2009 Archie L. Cobbs. All rights reserved.
 *
 * $Id: AtomicField.java 359 2010-02-24 21:26:08Z archie.cobbs $
 */

package org.dellroad.sidekar;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * A {@link SimpleField} whose Java representations are immutable objects and that
 * have an opaque, self-delimiting binary encoding.
 *
 * @param <T> Java type used to represent this field's value
 */
public abstract class AtomicField<T> extends SimpleField<T> {

    /**
     * Constructor.
     */
    AtomicField(String name, Entity<?> entity, Class<T> type) {
        super(name, entity, type);
    }

    @Override
    public boolean equals(T value1, T value2) {
        return value1 == value2 ? true : value1 == null ? value2 == null : value1.equals(value2);
    }

    @Override
    public final boolean canReference(Entity<?> target) {
        return false;
    }

    /**
     * Encode the given value in binary. The encoded bytes must be self-delimiting (i.e., encode their own length).
     */
    public abstract void encode(DataOutputStream output, T value) throws IOException;

    /**
     * Decode the binary encoding produced by {@link #encode}.
     *
     * @throws IllegalArgumentException if an invalid input byte is encountered
     * @throws EOFException if the input stream ends before decoding completes
     */
    public abstract T decode(DataInputStream input) throws IOException;

    /**
     * Convenience method.
     *
     * @see #encode(OutputStream, T)
     */
    public final byte[] encode(T value) {
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        DataOutputStream dataOutput = new DataOutputStream(output);
        try {
            this.encode(dataOutput, value);
            dataOutput.flush();
        } catch (IOException e) {
            throw new RuntimeException("unexpected exception");
        }
        return output.toByteArray();
    }

    /**
     * Convenience method.
     *
     * @param buf buffer containing data
     * @param off offset of the start of the encoded data
     * @throws IllegalArgumentException if the input bytes are invalid
     * @see #decode(InputStream)
     */
    public final T decode(byte[] buf, int off) {
        if (off >= buf.length)
            throw new IllegalArgumentException("truncated input");
        DataInputStream input = new DataInputStream(new ByteArrayInputStream(buf, off, buf.length - off));
        try {
            return this.decode(input);
        } catch (EOFException e) {
            throw new IllegalArgumentException("truncated input");
        } catch (IOException e) {
            throw new RuntimeException("unexpected exception");
        }
    }

    /**
     * Get the {@link AtomicField} type corresponding to the given Java class.
     *
     * @return the corresponding {@link AtomicField} class
     * @throws IllegalArgumentException if no such field type exists
     */
    @SuppressWarnings("unchecked")
    public static Class<? extends AtomicField<?>> getFieldType(Class<?> javaType) {
        try {
            return (Class<? extends AtomicField<?>>)FieldType.forJavaType(javaType).getFieldType().asSubclass(AtomicField.class);
        } catch (ClassCastException e) {
            throw new IllegalArgumentException("no known atomic field type for " + javaType);
        }
    }
}

